/*
 * Copyright 2008-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baturu.mybatisminus.injector.parser;

import com.baturu.mybatisminus.model.FieldInfo;
import com.baturu.mybatisminus.model.TableInfo;
import com.baturu.mybatisminus.util.Lists;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.*;

@EqualsAndHashCode
@Getter
public class Part {

	private final String property;
	private final Type type;
	private final FieldInfo fieldInfo;


	public Part(String source, TableInfo tableInfo) {

		Assert.hasText(source, "Part source must not be null or empty!");
		Assert.notNull(tableInfo, "TableInfo must not be null!");

        this.type = Type.fromProperty(source);
		this.property = extractAndCheckProperty(type, source, tableInfo);
		this.fieldInfo = tableInfo.getFieldInfo(property);
	}



	public int getRequireArguments() {
		return type.getNumberOfArguments();
	}


	@Override
	public String toString() {
		return String.format("%s %s", property, type);
	}


	private String extractAndCheckProperty(Type type, String source, TableInfo tableInfo) {
	    String property = type.extractProperty(source);
        if (!tableInfo.containsProperty(property)) {
            StringBuilder errMsg = new StringBuilder().append("Error parsing ").append(source).append(":")
                    .append(" property ").append(property).append(" is not existed in ").append(tableInfo.getEntityClass().getName());
            throw new IllegalArgumentException(errMsg.toString());
        }
        return property;
    }




    private static final String FOR_EACH = "\n<foreach item='item' index='index' collection='%s' open='(' separator=',' close=')'>\n#{item}\n</foreach>";

	public enum Type {

		BETWEEN(2, " %s between #{%s} and #{%s} ", Lists.newArrayList("IsBetween", "Between")),
		IS_NOT_NULL(0, " %s is not null ", Lists.newArrayList("IsNotNull", "NotNull")),
		IS_NULL(0, " %s is null ", Lists.newArrayList("IsNull", "Null")),
		LESS_THAN(" %s &lt; #{%s} ", Lists.newArrayList("IsLessThan", "LessThan")),
		LESS_THAN_EQUAL(" %s &lt;= #{%s} ", Lists.newArrayList("IsLessThanEqual", "LessThanEqual")),
		GREATER_THAN(" %s > #{%s} ", Lists.newArrayList("IsGreaterThan", "GreaterThan")),
		GREATER_THAN_EQUAL(" %s >= #{%s} ", Lists.newArrayList("IsGreaterThanEqual", "GreaterThanEqual")),
		NOT_LIKE(" %s not like #{%s} ", Lists.newArrayList("IsNotLike", "NotLike")),
		LIKE(" %s like #{%s} ", Lists.newArrayList("IsLike", "Like")),
		STARTING_WITH(" %s like concat(#{%s},'%%') ", Lists.newArrayList("IsStartingWith", "StartingWith", "StartsWith")),
		ENDING_WITH(" %s like concat('%%', #{%s}) ", Lists.newArrayList("IsEndingWith", "EndingWith", "EndsWith")),
		NOT_CONTAINING(" %s not like concat('%%', #{%s}, '%%') ", Lists.newArrayList("IsNotContaining", "NotContaining", "NotContains")),
		CONTAINING(" %s like concat('%%', #{%s}, '%%') ", Lists.newArrayList("IsContaining", "Containing", "Contains")),
		NOT_IN(" %s not in " + Part.FOR_EACH, Lists.newArrayList("IsNotIn", "NotIn")),
		IN(" %s in " + Part.FOR_EACH, Lists.newArrayList("IsIn", "In")),
		NEGATING_SIMPLE_PROPERTY(" %s != #{%s} ", Lists.newArrayList("IsNot", "Not")),
        SIMPLE_PROPERTY(" %s = #{%s} ", Lists.newArrayList("Is", "Equals")),
        BEFORE(" %s &lt; #{%s} ", Lists.newArrayList("IsBefore", "Before")),
        AFTER(" %s > #{%s} ", Lists.newArrayList("IsAfter", "After"));


        private static final List<Type> ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN, LESS_THAN_EQUAL,
                GREATER_THAN, GREATER_THAN_EQUAL, BEFORE, AFTER, NOT_LIKE, LIKE, STARTING_WITH, ENDING_WITH, NOT_CONTAINING, CONTAINING,
                NOT_IN, IN, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY);

        public static final Collection<String> ALL_KEYWORDS;

        static {
            List<String> allKeywords = new ArrayList<>();
            for (Type type : ALL) {
                allKeywords.addAll(type.keywords);
            }
            ALL_KEYWORDS = Collections.unmodifiableList(allKeywords);
        }

		private final List<String> keywords;
		private final int numberOfArguments;
		private final String sqlPattern;


		private Type(int numberOfArguments, String sqlPattern, List<String> keywords) {

			this.numberOfArguments = numberOfArguments;
			this.keywords = keywords;
			this.sqlPattern = sqlPattern;
		}

		private Type(String sqlPattern, List<String> keywords) {
			this(1, sqlPattern, keywords);
		}


		public static Part.Type fromProperty(String rawProperty) {

			for (Part.Type type : ALL) {
				if (type.supports(rawProperty)) {
					return type;
				}
			}
			return SIMPLE_PROPERTY;
		}

		public Collection<String> getKeywords() {
			return Collections.unmodifiableList(keywords);
		}

		public String getSqlPattern() {
		    return sqlPattern;
        }

		protected boolean supports(String property) {

			for (String keyword : keywords) {
				if (property.endsWith(keyword)) {
					return true;
				}
			}
			return false;
		}


		public int getNumberOfArguments() {
			return numberOfArguments;
		}


		public String extractProperty(String part) {

			String candidate = StringUtils.uncapitalize(part);

			for (String keyword : keywords) {
				if (candidate.endsWith(keyword)) {
					return candidate.substring(0, candidate.length() - keyword.length());
				}
			}
			return candidate;
		}


		@Override
		public String toString() {
			return String.format("%s (%s): %s", name(), getNumberOfArguments(), getKeywords());
		}
	}
}
